class Bitmap {
    constructor(width, height) {
        if(!this._defer){
            this._createCanvas(width, height);
        }

        this._image = null;
        this._url = '';
        this._paintOpacity = 255;
        this._smooth = false;
        this._loadListeners = [];
        this._loadingState = 'none';
        this._decodeAfterRequest = false;
        this.cacheEntry = null;
        this.fontFace = 'GameFont';
        this.fontSize = 28;
        this.fontItalic = false;
        this.textColor = '#ffffff';
        this.outlineColor = 'rgba(0, 0, 0, 0.5)';
        this.outlineWidth = 4;
    }

    _createCanvas(width, height) {
        this.__canvas = this.__canvas || document.createElement('canvas');
        this.__context = this.__canvas.getContext('2d');

        this.__canvas.width = Math.max(width || 0, 1);
        this.__canvas.height = Math.max(height || 0, 1);

        if(this._image){
            const w = Math.max(this._image.width || 0, 1);
            const h = Math.max(this._image.height || 0, 1);
            this.__canvas.width = w;
            this.__canvas.height = h;
            this._createBaseTexture(this._canvas);

            this.__context.drawImage(this._image, 0, 0);
        }
    }

    _createBaseTexture(source) {
        this.__baseTexture = new PIXI.BaseTexture(source);
        this.__baseTexture.mipmap = false;
        this.__baseTexture.width = source.width;
        this.__baseTexture.height = source.height;

        if (this._smooth) {
            this._baseTexture.scaleMode = PIXI.SCALE_MODES.LINEAR;
        } else {
            this._baseTexture.scaleMode = PIXI.SCALE_MODES.NEAREST;
        }
    }

    _clearImgInstance() {
        this._image.src = "";
        this._image.onload = null;
        this._image.onerror = null;
        this._errorListener = null;
        this._loadListener = null;

        Bitmap._reuseImages.push(this._image);
        this._image = null;
    }

    get _canvas() {
        if(!this.__canvas)this._createCanvas();
        return this.__canvas;
    }

    get _context() {
        if(!this.__context)this._createCanvas();
        return this.__context;
    }

    get _baseTexture() {
        if(!this.__baseTexture) this._createBaseTexture(this._image || this.__canvas);
        return this.__baseTexture;
    }

    _renewCanvas() {
        const newImage = this._image;
        if(newImage && this.__canvas && (this.__canvas.width < newImage.width || this.__canvas.height < newImage.height)){
            this._createCanvas();
        }
    }

    static load(url) {
        const bitmap = Object.create(Bitmap.prototype);
        bitmap._defer = true;
        bitmap.initialize();

        bitmap._decodeAfterRequest = true;
        bitmap._requestImage(url);

        return bitmap;
    }

    static snap(stage) {
        const width = Graphics.width;
        const height = Graphics.height;
        const bitmap = new Bitmap(width, height);
        const context = bitmap._context;
        const renderTexture = PIXI.RenderTexture.create(width, height);
        if (stage) {
            Graphics._renderer.render(stage, renderTexture);
            stage.worldTransform.identity();
            let canvas = null;
            if (Graphics.isWebGL()) {
                canvas = Graphics._renderer.extract.canvas(renderTexture);
            } else {
                canvas = renderTexture.baseTexture._canvasRenderTarget.canvas;
            }
            context.drawImage(canvas, 0, 0);
        } else {

        }
        renderTexture.destroy({ destroyBase: true });
        bitmap._setDirty();
        return bitmap;
    }

    isReady() {
        return this._loadingState === 'loaded' || this._loadingState === 'none';
    }

    isError() {
        return this._loadingState === 'error';
    }

    touch() {
        if (this.cacheEntry) {
            this.cacheEntry.touch();
        }
    }

    get url() {
        return this._url;
    }

    get baseTexture() {
        return this._baseTexture;
    }

    get canvas() {
        return this._canvas;
    }

    get context() {
        return this._context;
    }

    get width() {
        if(this.isReady()){
            return this._image? this._image.width: this._canvas.width;
        }

        return 0;
    }

    get height() {
        if(this.isReady()){
            return this._image? this._image.height: this._canvas.height;
        }

        return 0;
    }

    get rect() {
        return new Rectangle(0, 0, this.width, this.height);
    }

    get smooth() {
        return this._smooth;
    }

    set smooth(value) {
        if (this._smooth !== value) {
            this._smooth = value;
            if(this.__baseTexture){
                if (this._smooth) {
                    this._baseTexture.scaleMode = PIXI.SCALE_MODES.LINEAR;
                } else {
                    this._baseTexture.scaleMode = PIXI.SCALE_MODES.NEAREST;
                }
            }
        }
    }

    get paintOpacity() {
        return this._paintOpacity;
    }

    set paintOpacity(value) {
      if (this._paintOpacity !== value) {
          this._paintOpacity = value;
          this._context.globalAlpha = this._paintOpacity / 255;
      }
    }

    resize(width, height) {
        width = Math.max(width || 0, 1);
        height = Math.max(height || 0, 1);
        this._canvas.width = width;
        this._canvas.height = height;
        this._baseTexture.width = width;
        this._baseTexture.height = height;
    }

    blt(source, sx, sy, sw, sh, dx, dy, dw, dh) {
        dw = dw || sw;
        dh = dh || sh;
        if (sx >= 0 && sy >= 0 && sw > 0 && sh > 0 && dw > 0 && dh > 0 &&
                sx + sw <= source.width && sy + sh <= source.height) {
            this._context.globalCompositeOperation = 'source-over';
            this._context.drawImage(source._canvas, sx, sy, sw, sh, dx, dy, dw, dh);
            this._setDirty();
        }
    }

    bltImage(source, sx, sy, sw, sh, dx, dy, dw, dh) {
        dw = dw || sw;
        dh = dh || sh;
        if (sx >= 0 && sy >= 0 && sw > 0 && sh > 0 && dw > 0 && dh > 0 &&
            sx + sw <= source.width && sy + sh <= source.height) {
            this._context.globalCompositeOperation = 'source-over';
            this._context.drawImage(source._image, sx, sy, sw, sh, dx, dy, dw, dh);
            this._setDirty();
        }
    }

    getPixel(x, y) {
        const data = this._context.getImageData(x, y, 1, 1).data;
        let result = '#';
        for (let i = 0; i < 3; i++) {
            result += data[i].toString(16).padZero(2);
        }
        return result;
    }

    getAlphaPixel(x, y) {
        const data = this._context.getImageData(x, y, 1, 1).data;
        return data[3];
    }

    clearRect(x, y, width, height) {
        this._context.clearRect(x, y, width, height);
        this._setDirty();
    }

    clear() {
        this.clearRect(0, 0, this.width, this.height);
    }

    fillRect(x, y, width, height, color) {
        const context = this._context;
        context.save();
        context.fillStyle = color;
        context.fillRect(x, y, width, height);
        context.restore();
        this._setDirty();
    }

    fillAll(color) {
        this.fillRect(0, 0, this.width, this.height, color);
    }

    gradientFillRect(x, y, width, height, color1, color2, vertical) {
        const context = this._context;
        let grad;
        if (vertical) {
            grad = context.createLinearGradient(x, y, x, y + height);
        } else {
            grad = context.createLinearGradient(x, y, x + width, y);
        }
        grad.addColorStop(0, color1);
        grad.addColorStop(1, color2);
        context.save();
        context.fillStyle = grad;
        context.fillRect(x, y, width, height);
        context.restore();
        this._setDirty();
    }

    drawCircle(x, y, radius, color) {
        const context = this._context;
        context.save();
        context.fillStyle = color;
        context.beginPath();
        context.arc(x, y, radius, 0, Math.PI * 2, false);
        context.fill();
        context.restore();
        this._setDirty();
    }

    drawText(text, x, y, maxWidth, lineHeight, align) {
        if (text !== undefined) {
            let tx = x;
            const ty = y + lineHeight - (lineHeight - this.fontSize * 0.7) / 2;
            const context = this._context;
            const alpha = context.globalAlpha;
            maxWidth = maxWidth || 0xffffffff;
            if (align === 'center') {
                tx += maxWidth / 2;
            }
            if (align === 'right') {
                tx += maxWidth;
            }
            context.save();
            context.font = this._makeFontNameText();
            context.textAlign = align;
            context.textBaseline = 'alphabetic';
            context.globalAlpha = 1;
            this._drawTextOutline(text, tx, ty, maxWidth);
            context.globalAlpha = alpha;
            this._drawTextBody(text, tx, ty, maxWidth);
            context.restore();
            this._setDirty();
        }
    }

    measureTextWidth(text) {
        const context = this._context;
        context.save();
        context.font = this._makeFontNameText();
        const width = context.measureText(text).width;
        context.restore();
        return width;
    }

    adjustTone(r, g, b) {
        if ((r || g || b) && this.width > 0 && this.height > 0) {
            const context = this._context;
            const imageData = context.getImageData(0, 0, this.width, this.height);
            const pixels = imageData.data;
            for (let i = 0; i < pixels.length; i += 4) {
                pixels[i + 0] += r;
                pixels[i + 1] += g;
                pixels[i + 2] += b;
            }
            context.putImageData(imageData, 0, 0);
            this._setDirty();
        }
    }

    rotateHue(offset) {
        function rgbToHsl(r, g, b) {
            const cmin = Math.min(r, g, b);
            const cmax = Math.max(r, g, b);
            let h = 0;
            let s = 0;
            const l = (cmin + cmax) / 2;
            const delta = cmax - cmin;

            if (delta > 0) {
                if (r === cmax) {
                    h = 60 * (((g - b) / delta + 6) % 6);
                } else if (g === cmax) {
                    h = 60 * ((b - r) / delta + 2);
                } else {
                    h = 60 * ((r - g) / delta + 4);
                }
                s = delta / (255 - Math.abs(2 * l - 255));
            }
            return [h, s, l];
        }

        function hslToRgb(h, s, l) {
            const c = (255 - Math.abs(2 * l - 255)) * s;
            const x = c * (1 - Math.abs((h / 60) % 2 - 1));
            const m = l - c / 2;
            const cm = c + m;
            const xm = x + m;

            if (h < 60) {
                return [cm, xm, m];
            } else if (h < 120) {
                return [xm, cm, m];
            } else if (h < 180) {
                return [m, cm, xm];
            } else if (h < 240) {
                return [m, xm, cm];
            } else if (h < 300) {
                return [xm, m, cm];
            } else {
                return [cm, m, xm];
            }
        }

        if (offset && this.width > 0 && this.height > 0) {
            offset = ((offset % 360) + 360) % 360;
            const context = this._context;
            const imageData = context.getImageData(0, 0, this.width, this.height);
            const pixels = imageData.data;
            for (let i = 0; i < pixels.length; i += 4) {
                const hsl = rgbToHsl(pixels[i + 0], pixels[i + 1], pixels[i + 2]);
                const h = (hsl[0] + offset) % 360;
                const s = hsl[1];
                const l = hsl[2];
                const rgb = hslToRgb(h, s, l);
                pixels[i + 0] = rgb[0];
                pixels[i + 1] = rgb[1];
                pixels[i + 2] = rgb[2];
            }
            context.putImageData(imageData, 0, 0);
            this._setDirty();
        }
    }

    blur() {
        for (let i = 0; i < 2; i++) {
            const w = this.width;
            const h = this.height;
            const canvas = this._canvas;
            const context = this._context;
            const tempCanvas = document.createElement('canvas');
            const tempContext = tempCanvas.getContext('2d');
            tempCanvas.width = w + 2;
            tempCanvas.height = h + 2;
            tempContext.drawImage(canvas, 0, 0, w, h, 1, 1, w, h);
            tempContext.drawImage(canvas, 0, 0, w, 1, 1, 0, w, 1);
            tempContext.drawImage(canvas, 0, 0, 1, h, 0, 1, 1, h);
            tempContext.drawImage(canvas, 0, h - 1, w, 1, 1, h + 1, w, 1);
            tempContext.drawImage(canvas, w - 1, 0, 1, h, w + 1, 1, 1, h);
            context.save();
            context.fillStyle = 'black';
            context.fillRect(0, 0, w, h);
            context.globalCompositeOperation = 'lighter';
            context.globalAlpha = 1 / 9;
            for (let y = 0; y < 3; y++) {
                for (let x = 0; x < 3; x++) {
                    context.drawImage(tempCanvas, x, y, w, h, 0, 0, w, h);
                }
            }
            context.restore();
        }
        this._setDirty();
    }

    addLoadListener(listner) {
        if (!this.isReady()) {
            this._loadListeners.push(listner);
        } else {
            listner(this);
        }
    }

    _makeFontNameText() {
        return (this.fontItalic ? 'Italic ' : '') +
                this.fontSize + 'px ' + this.fontFace;
    }

    _drawTextOutline(text, tx, ty, maxWidth) {
        const context = this._context;
        context.strokeStyle = this.outlineColor;
        context.lineWidth = this.outlineWidth;
        context.lineJoin = 'round';
        context.strokeText(text, tx, ty, maxWidth);
    }

    _drawTextBody(text, tx, ty, maxWidth) {
        const context = this._context;
        context.fillStyle = this.textColor;
        context.fillText(text, tx, ty, maxWidth);
    }

    _onLoad() {
        this._image.removeEventListener('load', this._loadListener);
        this._image.removeEventListener('error', this._errorListener);

        this._renewCanvas();

        switch(this._loadingState){
            case 'requesting':
                this._loadingState = 'requestCompleted';
                if(this._decodeAfterRequest){
                    this.decode();
                }else{
                    this._loadingState = 'purged';
                    this._clearImgInstance();
                }
                break;

            case 'decrypting':
                window.URL.revokeObjectURL(this._image.src);
                this._loadingState = 'decryptCompleted';
                if(this._decodeAfterRequest){
                    this.decode();
                }else{
                    this._loadingState = 'purged';
                    this._clearImgInstance();
                }
                break;
        }
    }

    decode() {
        switch(this._loadingState){
            case 'requestCompleted': case 'decryptCompleted':
                this._loadingState = 'loaded';

                if(!this.__canvas) this._createBaseTexture(this._image);
                this._setDirty();
                this._callLoadListeners();
                break;

            case 'requesting': case 'decrypting':
                this._decodeAfterRequest = true;
                if (!this._loader) {
                    this._loader = ResourceHandler.createLoader(this._url, this._requestImage.bind(this, this._url), this._onError.bind(this));
                    this._image.removeEventListener('error', this._errorListener);
                    this._image.addEventListener('error', this._errorListener = this._loader);
                }
                break;

            case 'pending': case 'purged': case 'error':
                this._decodeAfterRequest = true;
                this._requestImage(this._url);
                break;
        }
    }

    _callLoadListeners() {
        while (this._loadListeners.length > 0) {
            const listener = this._loadListeners.shift();
            listener(this);
        }
    }

    _onError() {
        this._image.removeEventListener('load', this._loadListener);
        this._image.removeEventListener('error', this._errorListener);
        this._loadingState = 'error';
    }

    checkDirty() {
        if (this._dirty) {
            this._baseTexture.update();
            this._dirty = false;
        }
    }

    static request(url) {
        const bitmap = Object.create(Bitmap.prototype);
        bitmap._defer = true;
        bitmap.initialize();

        bitmap._url = url;
        bitmap._loadingState = 'pending';

        return bitmap;
    }

    _requestImage(url) {
        if(Bitmap._reuseImages.length !== 0){
            this._image = Bitmap._reuseImages.pop();
        }else{
            this._image = new Image();
        }

        if (this._decodeAfterRequest && !this._loader) {
            this._loader = ResourceHandler.createLoader(url, this._requestImage.bind(this, url), this._onError.bind(this));
        }

        this._image = new Image();
        this._url = url;
        this._loadingState = 'requesting';

        if(!Decrypter.checkImgIgnore(url) && Decrypter.hasEncryptedImages) {
            this._loadingState = 'decrypting';
            Decrypter.decryptImg(url, this);
        } else {
            this._image.src = url;

            this._image.addEventListener('load', this._loadListener = Bitmap.prototype._onLoad.bind(this));
            this._image.addEventListener('error', this._errorListener = this._loader || Bitmap.prototype._onError.bind(this));
        }
    }

    isRequestOnly() {
        return !(this._decodeAfterRequest || this.isReady());
    }

    isRequestReady() {
        return this._loadingState !== 'pending' &&
            this._loadingState !== 'requesting' &&
            this._loadingState !== 'decrypting';
    }

    startRequest() {
        if(this._loadingState === 'pending'){
            this._decodeAfterRequest = false;
            this._requestImage(this._url);
        }
    }
}

Bitmap._reuseImages = [];